package business;

import entity.Event;
import entity.Message;
import entity.MessageType;
import entity.Participant;
import entity.SentMessage;
import entity.SentMessagePK;
import entity.User;
import entity.Weather;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.Schedule;
import javax.ejb.Singleton;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import net.aksingh.java.api.owm.DailyForecastData;
import net.aksingh.java.api.owm.OpenWeatherMap;
import org.json.JSONException;

/**
 * The bean managing the weather forecasts. Update the forecasts of the events
 * any given period of time
 *
 * @author dami
 */
@Singleton
public class WeatherService {

    @PersistenceContext
    EntityManager em;

    /**
     * The method updating the forecasts periodically
     */
    //@Schedule(second = "*/30", minute = "*", hour = "*")
    @Schedule(hour = "*/3", persistent = true)
    private void updateForecasts() {
        //Number of days starting from today for which we want to update the forecast
        int nextDays = 10;
        //Retrieve all the forecasts in the database
        Calendar nextDaysCal = Calendar.getInstance();
        nextDaysCal.add(Calendar.DAY_OF_YEAR, nextDays);

        TypedQuery<Weather> findAfterDatePeriod = em.createQuery("SELECT w FROM Weather w WHERE (w.weatherDate BETWEEN :today AND :nextdays)", Weather.class);
        findAfterDatePeriod.setParameter("today", Calendar.getInstance().getTime());
        findAfterDatePeriod.setParameter("nextdays", nextDaysCal.getTime());

        //System.out.println("this is the timeStamp of searching after: " + Calendar.getInstance().getTime());
        //TypedQuery<Weather> findBeforeDatePeriod = em.createQuery("SELECT w FROM Weather w WHERE (w.weatherDate <= :nextdays)", Weather.class);
        //findBeforeDatePeriod.setParameter("nextdays", nextDaysCal.getTime());
        System.out.println("this is the timeStamp of searching before: " + nextDaysCal.getTime());

        //Retrieve a list of WEATHER to update
        List<Weather> weatherToUpdate = findAfterDatePeriod.getResultList();
        System.out.println("Sto per aggiornare tutti i weather : " + weatherToUpdate.toString());

        for (Weather w : weatherToUpdate) {
            System.out.println("Sto per  aggiornato uno dei weather: " + w.getId() + w.getCity());

            updateWeather(w);
            System.out.println("ho aggiornato il weather: " + w.getId() + w.getCity());

        }

        em.flush();

    }

    /**
     * Given a weather, the function updates its variable fields according to
     * the API
     *
     * @param weather the weather to update
     */
    private void updateWeather(Weather weather) {
        OpenWeatherMap owm = new OpenWeatherMap(OpenWeatherMap.OWM_URL.PARAMETER_UNITS_VALUE_METRIC, "475f2d874b5b1aded3861c7e7407f761");
        //Number of days we want the forecast to
        byte count = 16;
        //Update the date of updating of the WEATHER
        weather.setLastUpdate(Calendar.getInstance().getTime());
        //Retrieve a list of forecast for the next 16 days
        try {
            List<DailyForecastData.Forecast> forecast_list = owm.dailyForecastByCityName(weather.getCity(), count).getForecast_List();
            //If the forecasts have not been retrived it means that the city searched does not match with anyone in the weather API
            if (forecast_list.isEmpty()) {
                Logger.getLogger(EventManager.class.getName()).log(Level.INFO, "UNABLE TO RETRIEVE FORECAST FOR CITY: " + weather.getCity());
                em.merge(weather);
            } else {

                //Retrieve the forecast for the starting date of the event.
                Calendar weatherDateCal = Calendar.getInstance();
                weatherDateCal.setTime(weather.getWeatherDate());
                DailyForecastData.Forecast forecast = WeatherManager.matchForecast(forecast_list, weatherDateCal);

                //Perfectioning of the WEATHER entity 
                weather.setMaxTemp(forecast.getTemperature_Object().getMaximumTemperature());
                weather.setMinTemp(forecast.getTemperature_Object().getMinimumTemperature());
                weather.setWeatherCode(forecast.getWeather_List().get(0).getWeatherCode());
                weather.setWeatherDescription(forecast.getWeather_List().get(0).getWeatherDescription());
                //Save the WEATHER in database and return it to the caller
                em.merge(weather);
                System.out.println("Min temp: " + weather.getMinTemp() + " Max temp: " + weather.getMaxTemp() + " Desc: " + weather.getWeatherDescription());
            }

        } catch (IOException | JSONException ex) {
            //Error parsing the preForecast
            Logger.getLogger(EventManager.class.getName()).log(Level.INFO, "SERVICE TEMPORARY UNAVAILABLE");
            em.merge(weather);
        }
    }

    /**
     * The method in charge of advising people of the bad weather of an event.
     * It both advises participants if the event of the next day has a bad forecast
     * and advises creators of the events if the next three day event has a bad forecast.
     * In the last case it also suggest a sunny day if there is on within one week later.
     */
    //@Schedule (minute ="*/1", hour ="*")
    @Schedule(dayOfMonth="*", minute = "5", hour = "0", persistent = true)
    private void notifyBadWeather() {
        System.out.println("sono notifyBadWeather");

        TypedQuery<Weather> findWeatherPerDay = em.createQuery("SELECT w FROM Weather w WHERE (w.weatherDate = :day)", Weather.class);
        //Find all weather describing tomorrow weather
        //Set a Calendar type on tomorrow
        Calendar dayToControl = Calendar.getInstance();
        dayToControl.add(Calendar.DAY_OF_YEAR, 1);
        //Set parameter of the query on tomorrow
        findWeatherPerDay.setParameter("day", dayToControl.getTime());
        //Find the wanted list of weather:
        System.out.println("sono notifyBadWeather, cerco eventi da notificare 1 giorno");

        List<Weather> tomorrow_weatherList = findWeatherPerDay.getResultList();
        //Every Weather is to control: if bad, advice the participants to the events
        for (Weather w : tomorrow_weatherList) {
            System.out.println("sono notifyBadWeather, controllo e notifico un giorno per il weather: " + w.getWeatherCode());
            //advise participants because tomorrow is bad weather
            if (isBadWeather(w.getWeatherCode())) {
                //Notify the participants of the event:
                List<Event> badWeatherEvents = new ArrayList();
                badWeatherEvents.addAll(w.getEventCollection());
                notifyEventsParticipants(badWeatherEvents);
            }
        }

        //Find all events that are going to start in three days
        dayToControl.add(Calendar.DAY_OF_YEAR, 2);
        findWeatherPerDay.setParameter("day", dayToControl.getTime());
        List<Weather> threeDays_weatherList = findWeatherPerDay.getResultList();
        System.out.println("WEATHER DI EVENTI TRA TRE GIORNI CHE CONTROLLO SE SONO BRUTTI: "+threeDays_weatherList.toString());

        //Every Weather is to control: if bad, advice the participants to the events
        for (Weather w : threeDays_weatherList) {
            if (isBadWeather(w.getWeatherCode())) {
                //Notify the participants of the event:
                List<Event> badWeatherEvents = new ArrayList();
                badWeatherEvents.addAll(w.getEventCollection());
                System.out.println("PER OGNI WEATHER TRA 3 GIORNI VADO AD AVVISARE IL CREATORE DEGLI EVENTI CORRELATI DEL BRUTTO TEMPO: "+badWeatherEvents.toString());
                notifyEventsCreators(badWeatherEvents);
            }
        }

    }

    /**
     * The method that given a weatherCode say if it represents a bad weather.
     * @param weatherCode
     * @return true if the code is representing a "bad weather condition"
     */
    private boolean isBadWeather(int weatherCode) {
        System.out.println("sono isBadWeather");

        boolean bad1 = (weatherCode >= 200) && (weatherCode < 700);
        boolean bad2 = (weatherCode > 750) && (weatherCode < 790);
        boolean bad3 = (weatherCode >= 900) && (weatherCode <= 906);
        if (bad1 || bad2 || bad3) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Given the list of events the method for each event advises all participant there is a bad weather for that event.
     * @param events a list of events
     */
    private void notifyEventsParticipants(List<Event> events) {
        System.out.println("sono notifyEventsParticipants");

        //Notify participant for all events in the list
        for (Event e : events) {
            if (!e.isDeleted()) {
                //Send Message to all participants
                sendOneDayMessages(e.getParticipantCollection(), e);
            }
        }
    }

    /**
     * The method in charge of advising participants one day before an event occurs.
     * @param participants the collection of participants to advise
     * @param event the event that has a bad forecast
     */
    private void sendOneDayMessages(Collection<Participant> participants, Event event) {
        System.out.println("sono sendOneDayMessages");

        //create a message and set its fields
        Message message = new Message();
        message.setEvent(event);
        message.setType(MessageType.ONE_DAY);
        message.setTimestamp(Calendar.getInstance().getTime());
        em.persist(message);
        em.flush();
        //create the sentMessages
        int idMessage = em.find(Message.class, message.getMessageId()).getMessageId();
        for (Participant p : participants) {
            //create the key
            SentMessagePK smPK = new SentMessagePK();
            smPK.setUser(p.getUser1().getEmail());
            smPK.setMessage(idMessage);
            //create the sentMessage
            SentMessage sm = new SentMessage(smPK);
            sm.setPending(true);
            em.persist(sm);
            System.out.println("manadato mesaggio one day: " + sm.toString());
            em.flush();
        }
    }

    /**
     * The method in charge of advising the creators of the events about a bad weather forecast for their events
     * @param events the list of events we want to advise the creator of
     */
    private void notifyEventsCreators(List<Event> events) {
        System.out.println("sono notifyEventsCreators");

        for (Event e : events) {
            if (!e.isDeleted()) {
                sendThreeDaysMessages(e);
            }
        }

    }

    /**
     * The method that actually create and send messages at the creator of an event
     * @param event the event we want to advise the creator of
     */
    private void sendThreeDaysMessages(Event event) {
        System.out.println("sono sendThreeDaysMessages");

        //create a message and set its fields
        Message message = new Message();
        message.setEvent(event);
        message.setType(MessageType.THREE_DAYS);
        message.setTimestamp(Calendar.getInstance().getTime());
        //Search for a sunny day to suggest for the event
        Date suggestedDate = suggestDay(event);
        if (!suggestedDate.equals(event.getStartDate())) {
            message.setSugestedDate(suggestedDate);
        }
        em.persist(message);
        em.flush();
        int idMessage = em.find(Message.class, message.getMessageId()).getMessageId();
        //Send message to the creatore to advice him of bad bad weather
        //create the key
        SentMessagePK smPK = new SentMessagePK();
        smPK.setUser(event.getCreator().getEmail());
        smPK.setMessage(idMessage);
        //create the sentMessage
        SentMessage sm = new SentMessage(smPK);
        sm.setPending(true);
        em.persist(sm);
        System.out.println("manadato mesaggio 3 dayz: " + sm.toString());
        em.flush();
    }

    /**
     * The method searching for a sunny day within one week starting by the event starting date
     * @param event
     * @return the Date of a sunny day
     */
    private Date suggestDay(Event event) {
        System.out.println("sono suggestDay");

        OpenWeatherMap owm = new OpenWeatherMap(OpenWeatherMap.OWM_URL.PARAMETER_UNITS_VALUE_METRIC, "c63d0290a866387f9003506db7f1b8e9");
        //Number of days we want the forecast to
        byte count = 10;
        //Retrieve a list of forecast for the next 16 days
        try {
            List<DailyForecastData.Forecast> forecast_list = owm.dailyForecastByCityName(event.getCity(), count).getForecast_List();
            //If the forecasts have not been retrived it means that the city searched does not match with anyone in the weather API
            if (forecast_list.isEmpty()) {
                Logger.getLogger(EventManager.class.getName()).log(Level.INFO, "UNABLE TO RETRIEVE FORECAST FOR CITY: " + event.getCity());
                return event.getStartDate();
            } else {
                //Retrieve the forecast with sunny weather and after the starting date of the event
                return nextGoodDate(forecast_list, event.getStartDate());
            }
        } catch (IOException | JSONException ex) {
            //Error parsing the preForecast
            Logger.getLogger(EventManager.class.getName()).log(Level.INFO, "SERVICE TEMPORARY UNAVAILABLE");
            return event.getStartDate();
        }
    }

    /**
     * the method that actually choose a sunny day among a list of forecasts
     * @param fcList the list of forecasts among which we're searching for a good day
     * @param targetDate the date after which we want to find a sunny day
     * @return the Date of the sunny day found if there is one, else the starting date of the event.
     */
    private Date nextGoodDate(List<DailyForecastData.Forecast> fcList, Date targetDate) {
        System.out.println("sono nextGoodDate");
        for (DailyForecastData.Forecast fc : fcList) {
            if (fc.getDateTime().after(targetDate)) {
                if (!isBadWeather(fc.getWeather_List().get(0).getWeatherCode())) {
                    return fc.getDateTime();
                }
            }
        }
        return targetDate;
    }
    
}
